Introduction

I this notebook we ingest and visualize the mobility trends data provided by Apple, [APPL1].

We take the following steps:

  1. Download the data

  2. Import the data and summarise it

  3. Transform the data into long form

  4. Partition the data into subsets that correspond to combinations of geographical regions and transportation types

  5. Make contingency matrices and corresponding heat-map plots

  6. Make nearest neighbors graphs over the contingency matrices and plot communities

  7. Plot the corresponding time series

Data description

From Apple’s page https://www.apple.com/covid19/mobility

About This Data The CSV file and charts on this site show a relative volume of directions requests per country/region or city compared to a baseline volume on January 13th, 2020. We define our day as midnight-to-midnight, Pacific time. Cities represent usage in greater metropolitan areas and are stably defined during this period. In many countries/regions and cities, relative volume has increased since January 13th, consistent with normal, seasonal usage of Apple Maps. Day of week effects are important to normalize as you use this data. Data that is sent from users’ devices to the Maps service is associated with random, rotating identifiers so Apple doesn’t have a profile of your movements and searches. Apple Maps has no demographic information about our users, so we can’t make any statements about the representativeness of our usage against the overall population.

Observations

The observations listed in this subsection are also placed under the relevant statistics in the following sections and indicated with “Observation”.

  • The directions requests volumes reference date for normalization is 2020-01-13 : all the values in that column are \(100\).

  • From the community clusters of the nearest neighbor graphs (derived from the time series of the normalized driving directions requests volume) we see that countries and cities are clustered in expected ways. For example, in the community graph plot corresponding to “{city, driving}” the cities Oslo, Copenhagen, Helsinki, Stockholm, and Zurich are placed in the same cluster. In the graphs corresponding to “{city, transit}” and “{city, walking}” the Japanese cities Tokyo, Osaka, Nagoya, and Fukuoka are clustered together.

  • In the time series plots the Sundays are indicated with orange dashed lines. We can see that from Monday to Thursday people are more familiar with their trips than say on Fridays and Saturdays. We can also see that on Sundays people (on average) are more familiar with their trips or simply travel less.

Load packages

library(Matrix)
library(tidyverse)
Registered S3 methods overwritten by 'dbplyr':
  method         from
  print.tbl_lazy     
  print.tbl_sql      
── Attaching packages ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse 1.3.0 ──
✓ ggplot2 3.3.3     ✓ purrr   0.3.4
✓ tibble  3.0.6     ✓ dplyr   1.0.4
✓ tidyr   1.1.2     ✓ stringr 1.4.0
✓ readr   1.4.0     ✓ forcats 0.5.1
── Conflicts ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── tidyverse_conflicts() ──
x tidyr::expand() masks Matrix::expand()
x dplyr::filter() masks stats::filter()
x dplyr::lag()    masks stats::lag()
x tidyr::pack()   masks Matrix::pack()
x tidyr::unpack() masks Matrix::unpack()
library(ggplot2)
library(gridExtra)

Attaching package: ‘gridExtra’

The following object is masked from ‘package:dplyr’:

    combine
library(d3heatmap)
library(igraph)

Attaching package: ‘igraph’

The following objects are masked from ‘package:dplyr’:

    as_data_frame, groups, union

The following objects are masked from ‘package:purrr’:

    compose, simplify

The following object is masked from ‘package:tidyr’:

    crossing

The following object is masked from ‘package:tibble’:

    as_data_frame

The following objects are masked from ‘package:stats’:

    decompose, spectrum

The following object is masked from ‘package:base’:

    union
library(zoo)

Attaching package: ‘zoo’

The following objects are masked from ‘package:base’:

    as.Date, as.Date.numeric

Data ingestion

Apple mobile data was provided in this WWW page: https://www.apple.com/covid19/mobility , [APPL1]. (The data has to be download from that web page – there is an “agreement to terms”, etc.)

dfAppleMobility <- read.csv( "~/Downloads/applemobilitytrends-2021-01-15.csv", stringsAsFactors = FALSE)
#dfAppleMobility <- read.csv("https://covid19-static.cdn-apple.com/covid19-mobility-data/2024HotfixDev18/v3/en-us/applemobilitytrends-2021-01-15.csv")
names(dfAppleMobility) <- gsub( "^X", "", names(dfAppleMobility))
names(dfAppleMobility) <- gsub( ".", "-", names(dfAppleMobility), fixed = TRUE)
dfAppleMobility

Observation: The directions requests volumes reference date for normalization is 2020-01-13 : all the values in that column are \(100\).

Data dimensions:

dim(dfAppleMobility)
[1] 4691  375

Data summary:

summary(as.data.frame(unclass(dfAppleMobility[,1:3]), stringsAsFactors = TRUE))
           geo_type                  region     transportation_type
 city          : 790   Washington County:  27   driving:3048       
 country/region: 153   Jefferson County :  25   transit: 551       
 county        :2638   Montgomery County:  24   walking:1092       
 sub-region    :1110   Franklin County  :  22                      
                       Madison County   :  21                      
                       Jackson County   :  19                      
                       (Other)          :4553                      

Number of unique “country/region” values:

dfAppleMobility %>% 
  dplyr::filter( geo_type == "country/region") %>% 
  dplyr::pull("region") %>%
  unique %>% 
  length
[1] 63

Number of unique “city” values:

dfAppleMobility %>% 
  dplyr::filter( geo_type == "city") %>% 
  dplyr::pull("region") %>%
  unique %>% 
  length
[1] 295

All unique geo types:

lsGeoTypes <- unique(dfAppleMobility[["geo_type"]])
lsGeoTypes
[1] "country/region" "city"           "sub-region"     "county"        

All unique transportation types:

lsTransportationTypes <-  unique(dfAppleMobility[["transportation_type"]])
lsTransportationTypes
[1] "driving" "walking" "transit"

Data transformation

It is better to have the data in long form (narrow form). For that I am using the package “tidyr”.

# lsIDColumnNames <- c("geo_type", "region", "transportation_type") # For the initial dataset released by Apple.
lsIDColumnNames <- c("geo_type", "region", "transportation_type", "alternative_name", "sub-region", "country" )
dfAppleMobilityLongForm <- tidyr::pivot_longer( data = dfAppleMobility, cols = setdiff( names(dfAppleMobility), lsIDColumnNames), names_to = "Date", values_to = "Value" )
dim(dfAppleMobilityLongForm)
[1] 1730979       8

Remove the rows with “empty” values:

dfAppleMobilityLongForm <- dfAppleMobilityLongForm[ complete.cases(dfAppleMobilityLongForm), ]
dim(dfAppleMobilityLongForm)
[1] 1709416       8

Add the “DateObject” column:

dfAppleMobilityLongForm$DateObject <- as.POSIXct( dfAppleMobilityLongForm$Date, format = "%Y-%m-%d", origin = "1970-01-01" )

Add “day name” (“day of the week”) field:

dfAppleMobilityLongForm$DayName <- weekdays(dfAppleMobilityLongForm$DateObject)

Here is sample of the transformed data:

set.seed(3232)
dfAppleMobilityLongForm %>% dplyr::sample_n( 10 )

Here is summary:

summary(as.data.frame(unclass(dfAppleMobilityLongForm), stringsAsFactors = TRUE))
           geo_type                    region        transportation_type                alternative_name        sub.region              country                Date             Value           DateObject                       DayName      
 city          :289938   Washington County:   9919   driving:1104303                            :1335188             :486577   United States:1139718   2020-01-13:   4652   Min.   :   0.44   Min.   :2020-01-13 00:00:00   Friday   :246556  
 country/region: 56151   Jefferson County :   9187   transit: 202875     AB                     :   1105   Texas     : 88523   Japan        :  81295   2020-01-14:   4652   1st Qu.:  84.26   1st Qu.:2020-04-13 00:00:00   Monday   :242970  
 county        :969242   Montgomery County:   8826   walking: 402238     ACT                    :   1105   California: 61030                :  56151   2020-01-15:   4652   Median : 113.54   Median :2020-07-16 00:00:00   Saturday :241904  
 sub-region    :394085   Franklin County  :   8078                       Andalucía              :   1105   Georgia   : 48119   France       :  33098   2020-01-16:   4652   Mean   : 121.59   Mean   :2020-07-15 06:51:04   Sunday   :241904  
                         Madison County   :   7713                       Bayern                 :   1105   Virginia  : 45183   Germany      :  31608   2020-01-17:   4652   3rd Qu.: 148.41   3rd Qu.:2020-10-16 00:00:00   Thursday :246556  
                         Jackson County   :   6979                       BC|Colombie-Britannique:   1105   Florida   : 44493   Thailand     :  24968   2020-01-18:   4652   Max.   :2148.12   Max.   :2021-01-15 00:00:00   Tuesday  :242970  
                         (Other)          :1658714                       (Other)                : 368703   (Other)   :935491   (Other)      : 342578   (Other)   :1681504                                                   Wednesday:246556  

Partition the data into geo types × transportation types:

dfAppleMobilityLongForm %>% 
  dplyr::group_by( geo_type, transportation_type) %>% 
  dplyr::count()
aQueries <- split(dfAppleMobilityLongForm,  dfAppleMobilityLongForm[,c("geo_type", "transportation_type")] )

Heat-map plots

We can visualize the data using heat-map plots.

Remark: Using the contingency matrices prepared for the heat-map plots we can do further analysis, like, finding correlations or nearest neighbors. (See below.)

Cross-tabulate dates with regions:

aMatDateRegion <- purrr::map( aQueries, function(dfX) { xtabs( formula = Value ~ Date + region, data = dfX, sparse = TRUE ) } )
aMatDateRegion <- aMatDateRegion[ purrr::map_lgl(aMatDateRegion, function(x) nrow(x) > 0 ) ]
dfPlotQuery <- purrr::map_df( aMatDateRegion, Matrix::summary, .id = "Type" )
head(dfPlotQuery)
367 x 295 sparse Matrix of class "dgCMatrix", with 108265 entries 
          Type i j      x
1 city.driving 1 1 100.00
2 city.driving 2 1 100.73
3 city.driving 3 1 102.86
4 city.driving 4 1 102.65
5 city.driving 5 1 109.39
6 city.driving 6 1 109.62
ggplot2::ggplot(dfPlotQuery) +
  ggplot2::geom_tile( ggplot2::aes( x = j, y = i, fill = log10(x)), color = "white") +
  ggplot2::scale_fill_gradient(low = "white", high = "blue") +
  ggplot2::xlab("Region") + ggplot2::ylab("Date") + 
  ggplot2::facet_wrap( ~Type, scales = "free", ncol = 2)

Here we take a “closer look” to one of the plots using a dedicated d3heatmap plot:

d3heatmap::d3heatmap( x = aMatDateRegion[["country/region.driving"]], Rowv = FALSE )

Nearest neighbors graphs

Graphs overview

Here we create nearest neighbor graphs of the contingency matrices computed above and plot cluster the nodes:

th <- 0.94
aNNGraphs <- 
  purrr::map( aMatDateRegion, function(m) { 
    m2 <- cor(as.matrix(m))
    for( i in 1:nrow(m2) ) {
      m2[i,i] <- 0
    }
    m2 <- as( m2, "dgCMatrix") 
    m2@x[ m2@x <= th ] <- 0
    #m2@x[ m2@x > th ] <- 1
    igraph::graph_from_adjacency_matrix(Matrix::drop0(m2), weighted = TRUE, mode = "undirected")
  })
ind <- 3
ceb <- cluster_edge_betweenness(aNNGraphs[[ind]])  
dendPlot(ceb, mode="hclust", main = names(aNNGraphs)[[ind]])
plot(ceb, aNNGraphs[[ind]], vertex.size=1, vertex.label=NA, main = names(aNNGraphs)[[ind]])

Time series analysis

Time series

In this section for each date we sum all cases over the region-transportation pairs, make a time series, and plot them.

Remark: In the plots the Sundays are indicated with orange dashed lines.

Here we make the time series:

aDateStringToDateObject <- unique( dfAppleMobilityLongForm[, c("Date", "DateObject")] )
aDateStringToDateObject <- setNames( aDateStringToDateObject$DateObject, aDateStringToDateObject$Date )
aDateStringToDateObject <- as.POSIXct(aDateStringToDateObject)
aTSDirReqByCountry <-  purrr::map( aMatDateRegion, function(m) rowSums(m) )
matTS <- do.call( cbind, aTSDirReqByCountry)
number of rows of result is not a multiple of vector length (arg 1)
zooObj <- zoo::zoo( x = matTS, as.POSIXct(rownames(matTS)) )

Here we plot them:

autoplot(zooObj) +
  aes(colour = NULL, linetype = NULL) +
    facet_grid(Series ~ ., scales = "free_y") +
  geom_vline( xintercept = aDateStringToDateObject[weekdays(aDateStringToDateObject) == "Sunday"], color = "orange", linetype = "dashed", size = 0.3 )

Observation: In the time series plots the Sundays are indicated with orange dashed lines. We can see that from Monday to Thursday people are more familiar with their trips than say on Fridays and Saturdays. We can also see that on Sundays people (on average) are more familiar with their trips or simply travel less.

“Forecast”

He we do “forecast” for code-workflow demonstration purposes – the forecasts should not be taken seriously.

Fit a time series model to the time series:

aTSModels <- purrr::map( names(zooObj), function(x) { forecast::auto.arima( zoo( x = zooObj[,x], order.by = index(zooObj) ) ) } )
aTSModels <- purrr::map( names(zooObj), function(x) forecast::forecast( as.matrix(zooObj)[,x] ) )
names(aTSModels) <- names(zooObj)

Plot data and forecast:

lsPlots <- purrr::map( names(aTSModels), function(x) autoplot(aTSModels[[x]]) + ylab("Volume") + ggtitle(x) )
names(lsPlots) <- names(aTSModels)
do.call( gridExtra::grid.arrange, lsPlots )

References

[APPL1] Apple Inc., Mobility Trends Reports, (2020), apple.com.

[AA1] Anton Antonov, “Apple mobility trends data visualization”, (2020), SystemModeling at GitHub.

[AA2] Anton Antonov, “NY Times COVID-19 data visualization”, (2020), SystemModeling at GitHub.

LS0tCnRpdGxlOiAiQXBwbGUgbW9iaWxpdHkgdHJlbmRzIGRhdGEgdmlzdWFsaXphdGlvbiIKYXV0aG9yOiBBbnRvbiBBbnRvbm92CmRhdGU6IDIwMjAtMDUtMTMKb3V0cHV0OiBodG1sX25vdGVib29rCi0tLQoKPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KLm1haW4tY29udGFpbmVyIHsKICBtYXgtd2lkdGg6IDE4MDBweDsKICBtYXJnaW4tbGVmdDogYXV0bzsKICBtYXJnaW4tcmlnaHQ6IGF1dG87Cn0KPC9zdHlsZT4KCgojIEludHJvZHVjdGlvbgoKSSB0aGlzIG5vdGVib29rIHdlIGluZ2VzdCBhbmQgdmlzdWFsaXplIHRoZSBtb2JpbGl0eSB0cmVuZHMgZGF0YSBwcm92aWRlZCBieSBBcHBsZSwgW0FQUEwxXS4KCldlIHRha2UgdGhlIGZvbGxvd2luZyBzdGVwczoKCjEuIERvd25sb2FkIHRoZSBkYXRhCgoyLiBJbXBvcnQgdGhlIGRhdGEgYW5kIHN1bW1hcmlzZSBpdAoKMy4gVHJhbnNmb3JtIHRoZSBkYXRhIGludG8gbG9uZyBmb3JtCgo0LiBQYXJ0aXRpb24gdGhlIGRhdGEgaW50byBzdWJzZXRzIHRoYXQgY29ycmVzcG9uZCB0byBjb21iaW5hdGlvbnMgb2YgZ2VvZ3JhcGhpY2FsIHJlZ2lvbnMgYW5kIHRyYW5zcG9ydGF0aW9uIHR5cGVzCgo1LiBNYWtlIGNvbnRpbmdlbmN5IG1hdHJpY2VzIGFuZCBjb3JyZXNwb25kaW5nIGhlYXQtbWFwIHBsb3RzCgo2LiBNYWtlIG5lYXJlc3QgbmVpZ2hib3JzIGdyYXBocyBvdmVyIHRoZSBjb250aW5nZW5jeSBtYXRyaWNlcyBhbmQgcGxvdCBjb21tdW5pdGllcwoKNy4gUGxvdCB0aGUgY29ycmVzcG9uZGluZyB0aW1lIHNlcmllcwoKIyMgRGF0YSBkZXNjcmlwdGlvbgoKIyMjIEZyb20gQXBwbGXigJlzIHBhZ2UgW2h0dHBzOi8vd3d3LmFwcGxlLmNvbS9jb3ZpZDE5L21vYmlsaXR5XShodHRwczovL3d3dy5hcHBsZS5jb20vY292aWQxOS9tb2JpbGl0eSkKCioqQWJvdXQgVGhpcyBEYXRhKioKVGhlIENTViBmaWxlIGFuZCBjaGFydHMgb24gdGhpcyBzaXRlIHNob3cgYSByZWxhdGl2ZSB2b2x1bWUgb2YgZGlyZWN0aW9ucyByZXF1ZXN0cyBwZXIgY291bnRyeS9yZWdpb24gb3IgY2l0eSBjb21wYXJlZCB0byBhIGJhc2VsaW5lIHZvbHVtZSBvbiBKYW51YXJ5IDEzdGgsIDIwMjAuCldlIGRlZmluZSBvdXIgZGF5IGFzIG1pZG5pZ2h0LXRvLW1pZG5pZ2h0LCBQYWNpZmljIHRpbWUuIENpdGllcyByZXByZXNlbnQgdXNhZ2UgaW4gZ3JlYXRlciBtZXRyb3BvbGl0YW4gYXJlYXMgYW5kIGFyZSBzdGFibHkgZGVmaW5lZCBkdXJpbmcgdGhpcyBwZXJpb2QuIEluIG1hbnkgY291bnRyaWVzL3JlZ2lvbnMgYW5kIGNpdGllcywgcmVsYXRpdmUgdm9sdW1lIGhhcyBpbmNyZWFzZWQgc2luY2UgSmFudWFyeSAxM3RoLCBjb25zaXN0ZW50IHdpdGggbm9ybWFsLCBzZWFzb25hbCB1c2FnZSBvZiBBcHBsZSBNYXBzLiBEYXkgb2Ygd2VlayBlZmZlY3RzIGFyZSBpbXBvcnRhbnQgdG8gbm9ybWFsaXplIGFzIHlvdSB1c2UgdGhpcyBkYXRhLgpEYXRhIHRoYXQgaXMgc2VudCBmcm9tIHVzZXJz4oCZIGRldmljZXMgdG8gdGhlIE1hcHMgc2VydmljZSBpcyBhc3NvY2lhdGVkIHdpdGggcmFuZG9tLCByb3RhdGluZyBpZGVudGlmaWVycyBzbyBBcHBsZSBkb2VzbuKAmXQgaGF2ZSBhIHByb2ZpbGUgb2YgeW91ciBtb3ZlbWVudHMgYW5kIHNlYXJjaGVzLiBBcHBsZSBNYXBzIGhhcyBubyBkZW1vZ3JhcGhpYyBpbmZvcm1hdGlvbiBhYm91dCBvdXIgdXNlcnMsIHNvIHdlIGNhbuKAmXQgbWFrZSBhbnkgc3RhdGVtZW50cyBhYm91dCB0aGUgcmVwcmVzZW50YXRpdmVuZXNzIG9mIG91ciB1c2FnZSBhZ2FpbnN0IHRoZSBvdmVyYWxsIHBvcHVsYXRpb24uCgojIyBPYnNlcnZhdGlvbnMKClRoZSBvYnNlcnZhdGlvbnMgbGlzdGVkIGluIHRoaXMgc3Vic2VjdGlvbiBhcmUgYWxzbyBwbGFjZWQgdW5kZXIgdGhlIHJlbGV2YW50IHN0YXRpc3RpY3MgaW4gdGhlIGZvbGxvd2luZyBzZWN0aW9ucyBhbmQgaW5kaWNhdGVkIHdpdGgg4oCcKipPYnNlcnZhdGlvbioq4oCdLgoKLSBUaGUgZGlyZWN0aW9ucyByZXF1ZXN0cyB2b2x1bWVzIHJlZmVyZW5jZSBkYXRlIGZvciBub3JtYWxpemF0aW9uIGlzIDIwMjAtMDEtMTMgOiBhbGwgdGhlIHZhbHVlcyBpbiB0aGF0IGNvbHVtbiBhcmUgJDEwMCQuCgotIEZyb20gdGhlIGNvbW11bml0eSBjbHVzdGVycyBvZiB0aGUgbmVhcmVzdCBuZWlnaGJvciBncmFwaHMgKGRlcml2ZWQgZnJvbSB0aGUgdGltZSBzZXJpZXMgb2YgdGhlIG5vcm1hbGl6ZWQgZHJpdmluZyBkaXJlY3Rpb25zIHJlcXVlc3RzIHZvbHVtZSkgd2Ugc2VlIHRoYXQgY291bnRyaWVzIGFuZCBjaXRpZXMgYXJlIGNsdXN0ZXJlZCBpbiBleHBlY3RlZCB3YXlzLiBGb3IgZXhhbXBsZSwgaW4gdGhlIGNvbW11bml0eSBncmFwaCBwbG90IGNvcnJlc3BvbmRpbmcgdG8g4oCce2NpdHksIGRyaXZpbmd94oCdIHRoZSBjaXRpZXMgT3NsbywgQ29wZW5oYWdlbiwgSGVsc2lua2ksIFN0b2NraG9sbSwgYW5kIFp1cmljaCBhcmUgcGxhY2VkIGluIHRoZSBzYW1lIGNsdXN0ZXIuIEluIHRoZSBncmFwaHMgY29ycmVzcG9uZGluZyB0byDigJx7Y2l0eSwgdHJhbnNpdH3igJ0gYW5kIOKAnHtjaXR5LCB3YWxraW5nfeKAnSB0aGUgSmFwYW5lc2UgY2l0aWVzIFRva3lvLCBPc2FrYSwgTmFnb3lhLCBhbmQgRnVrdW9rYSBhcmUgY2x1c3RlcmVkIHRvZ2V0aGVyLgoKLSBJbiB0aGUgdGltZSBzZXJpZXMgcGxvdHMgdGhlIFN1bmRheXMgYXJlIGluZGljYXRlZCB3aXRoIG9yYW5nZSBkYXNoZWQgbGluZXMuIFdlIGNhbiBzZWUgdGhhdCBmcm9tIE1vbmRheSB0byBUaHVyc2RheSBwZW9wbGUgYXJlIG1vcmUgZmFtaWxpYXIgd2l0aCB0aGVpciB0cmlwcyB0aGFuIHNheSBvbiBGcmlkYXlzIGFuZCBTYXR1cmRheXMuIFdlIGNhbiBhbHNvIHNlZSB0aGF0IG9uIFN1bmRheXMgcGVvcGxlIChvbiBhdmVyYWdlKSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoZWlyIHRyaXBzIG9yIHNpbXBseSB0cmF2ZWwgbGVzcy4KCiMgTG9hZCBwYWNrYWdlcwoKYGBge3J9CmxpYnJhcnkoTWF0cml4KQpsaWJyYXJ5KHRpZHl2ZXJzZSkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGdyaWRFeHRyYSkKbGlicmFyeShkM2hlYXRtYXApCmxpYnJhcnkoaWdyYXBoKQpsaWJyYXJ5KHpvbykKYGBgCgoKIyMgRGF0YSBpbmdlc3Rpb24KCkFwcGxlIG1vYmlsZSBkYXRhIHdhcyBwcm92aWRlZCBpbiB0aGlzIFdXVyBwYWdlOiBbaHR0cHM6Ly93d3cuYXBwbGUuY29tL2NvdmlkMTkvbW9iaWxpdHldKGh0dHBzOi8vd3d3LmFwcGxlLmNvbS9jb3ZpZDE5L21vYmlsaXR5KSAsIFtBUFBMMV0uIChUaGUgZGF0YSBoYXMgdG8gYmUgZG93bmxvYWQgZnJvbSB0aGF0IHdlYiBwYWdlIC0tIHRoZXJlIGlzIGFuIOKAnGFncmVlbWVudCB0byB0ZXJtc+KAnSwgZXRjLikKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHkgPC0gcmVhZC5jc3YoICJ+L0Rvd25sb2Fkcy9hcHBsZW1vYmlsaXR5dHJlbmRzLTIwMjEtMDEtMTUuY3N2Iiwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKQojZGZBcHBsZU1vYmlsaXR5IDwtIHJlYWQuY3N2KCJodHRwczovL2NvdmlkMTktc3RhdGljLmNkbi1hcHBsZS5jb20vY292aWQxOS1tb2JpbGl0eS1kYXRhLzIwMjRIb3RmaXhEZXYxOC92My9lbi11cy9hcHBsZW1vYmlsaXR5dHJlbmRzLTIwMjEtMDEtMTUuY3N2IikKbmFtZXMoZGZBcHBsZU1vYmlsaXR5KSA8LSBnc3ViKCAiXlgiLCAiIiwgbmFtZXMoZGZBcHBsZU1vYmlsaXR5KSkKbmFtZXMoZGZBcHBsZU1vYmlsaXR5KSA8LSBnc3ViKCAiLiIsICItIiwgbmFtZXMoZGZBcHBsZU1vYmlsaXR5KSwgZml4ZWQgPSBUUlVFKQpgYGAKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHkKYGBgCgoKKipPYnNlcnZhdGlvbjoqKiBUaGUgZGlyZWN0aW9ucyByZXF1ZXN0cyB2b2x1bWVzIHJlZmVyZW5jZSBkYXRlIGZvciBub3JtYWxpemF0aW9uIGlzIDIwMjAtMDEtMTMgOiBhbGwgdGhlIHZhbHVlcyBpbiB0aGF0IGNvbHVtbiBhcmUgJDEwMCQuCgpEYXRhIGRpbWVuc2lvbnM6CgpgYGB7cn0KZGltKGRmQXBwbGVNb2JpbGl0eSkKYGBgCgpEYXRhIHN1bW1hcnk6CgpgYGB7cn0Kc3VtbWFyeShhcy5kYXRhLmZyYW1lKHVuY2xhc3MoZGZBcHBsZU1vYmlsaXR5WywxOjNdKSwgc3RyaW5nc0FzRmFjdG9ycyA9IFRSVUUpKQpgYGAKCk51bWJlciBvZiB1bmlxdWUg4oCcY291bnRyeS9yZWdpb27igJ0gdmFsdWVzOgoKYGBge3J9CmRmQXBwbGVNb2JpbGl0eSAlPiUgCiAgZHBseXI6OmZpbHRlciggZ2VvX3R5cGUgPT0gImNvdW50cnkvcmVnaW9uIikgJT4lIAogIGRwbHlyOjpwdWxsKCJyZWdpb24iKSAlPiUKICB1bmlxdWUgJT4lIAogIGxlbmd0aApgYGAKCk51bWJlciBvZiB1bmlxdWUg4oCcY2l0eeKAnSB2YWx1ZXM6CgpgYGB7cn0KZGZBcHBsZU1vYmlsaXR5ICU+JSAKICBkcGx5cjo6ZmlsdGVyKCBnZW9fdHlwZSA9PSAiY2l0eSIpICU+JSAKICBkcGx5cjo6cHVsbCgicmVnaW9uIikgJT4lCiAgdW5pcXVlICU+JSAKICBsZW5ndGgKYGBgCgoKQWxsIHVuaXF1ZSBnZW8gdHlwZXM6CgpgYGB7cn0KbHNHZW9UeXBlcyA8LSB1bmlxdWUoZGZBcHBsZU1vYmlsaXR5W1siZ2VvX3R5cGUiXV0pCmxzR2VvVHlwZXMKYGBgCgpBbGwgdW5pcXVlIHRyYW5zcG9ydGF0aW9uIHR5cGVzOgoKYGBge3J9CmxzVHJhbnNwb3J0YXRpb25UeXBlcyA8LSAgdW5pcXVlKGRmQXBwbGVNb2JpbGl0eVtbInRyYW5zcG9ydGF0aW9uX3R5cGUiXV0pCmxzVHJhbnNwb3J0YXRpb25UeXBlcwpgYGAKCiMgRGF0YSB0cmFuc2Zvcm1hdGlvbgoKSXQgaXMgYmV0dGVyIHRvIGhhdmUgdGhlIGRhdGEgaW4gW2xvbmcgZm9ybSAobmFycm93IGZvcm0pXShodHRwczovL2VuLndpa2lwZWRpYS5vcmcvd2lraS9XaWRlX2FuZF9uYXJyb3dfZGF0YSkuIApGb3IgdGhhdCBJIGFtIHVzaW5nIHRoZSBwYWNrYWdlIFsidGlkeXIiXShodHRwczovL3RpZHlyLnRpZHl2ZXJzZS5vcmcpLgoKYGBge3J9CiMgbHNJRENvbHVtbk5hbWVzIDwtIGMoImdlb190eXBlIiwgInJlZ2lvbiIsICJ0cmFuc3BvcnRhdGlvbl90eXBlIikgIyBGb3IgdGhlIGluaXRpYWwgZGF0YXNldCByZWxlYXNlZCBieSBBcHBsZS4KbHNJRENvbHVtbk5hbWVzIDwtIGMoImdlb190eXBlIiwgInJlZ2lvbiIsICJ0cmFuc3BvcnRhdGlvbl90eXBlIiwgImFsdGVybmF0aXZlX25hbWUiLCAic3ViLXJlZ2lvbiIsICJjb3VudHJ5IiApCmRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtIDwtIHRpZHlyOjpwaXZvdF9sb25nZXIoIGRhdGEgPSBkZkFwcGxlTW9iaWxpdHksIGNvbHMgPSBzZXRkaWZmKCBuYW1lcyhkZkFwcGxlTW9iaWxpdHkpLCBsc0lEQ29sdW1uTmFtZXMpLCBuYW1lc190byA9ICJEYXRlIiwgdmFsdWVzX3RvID0gIlZhbHVlIiApCmRpbShkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSkKYGBgCgpSZW1vdmUgdGhlIHJvd3Mgd2l0aCDigJxlbXB0eeKAnSB2YWx1ZXM6CgpgYGB7cn0KZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0gPC0gZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm1bIGNvbXBsZXRlLmNhc2VzKGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtKSwgXQpkaW0oZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0pCmBgYAoKQWRkIHRoZSAiRGF0ZU9iamVjdCIgY29sdW1uOgoKYGBge3J9CmRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtJERhdGVPYmplY3QgPC0gYXMuUE9TSVhjdCggZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0kRGF0ZSwgZm9ybWF0ID0gIiVZLSVtLSVkIiwgb3JpZ2luID0gIjE5NzAtMDEtMDEiICkKYGBgCgpBZGQgImRheSBuYW1lIiAo4oCcZGF5IG9mIHRoZSB3ZWVr4oCdKSBmaWVsZDoKCmBgYHtyfQpkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSREYXlOYW1lIDwtIHdlZWtkYXlzKGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtJERhdGVPYmplY3QpCmBgYAoKSGVyZSBpcyBzYW1wbGUgb2YgdGhlIHRyYW5zZm9ybWVkIGRhdGE6CgpgYGB7cn0Kc2V0LnNlZWQoMzIzMikKZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0gJT4lIGRwbHlyOjpzYW1wbGVfbiggMTAgKQpgYGAKCkhlcmUgaXMgc3VtbWFyeToKCmBgYHtyfQpzdW1tYXJ5KGFzLmRhdGEuZnJhbWUodW5jbGFzcyhkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSksIHN0cmluZ3NBc0ZhY3RvcnMgPSBUUlVFKSkKYGBgCgpQYXJ0aXRpb24gdGhlIGRhdGEgaW50byBnZW8gdHlwZXMgw5cgdHJhbnNwb3J0YXRpb24gdHlwZXM6CgpgYGB7cn0KZGZBcHBsZU1vYmlsaXR5TG9uZ0Zvcm0gJT4lIAogIGRwbHlyOjpncm91cF9ieSggZ2VvX3R5cGUsIHRyYW5zcG9ydGF0aW9uX3R5cGUpICU+JSAKICBkcGx5cjo6Y291bnQoKQpgYGAKCmBgYHtyfQphUXVlcmllcyA8LSBzcGxpdChkZkFwcGxlTW9iaWxpdHlMb25nRm9ybSwgIGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtWyxjKCJnZW9fdHlwZSIsICJ0cmFuc3BvcnRhdGlvbl90eXBlIildICkKYGBgCgojIEhlYXQtbWFwIHBsb3RzCgpXZSBjYW4gdmlzdWFsaXplIHRoZSBkYXRhIHVzaW5nIGhlYXQtbWFwIHBsb3RzLgoKKipSZW1hcms6KiogVXNpbmcgdGhlIGNvbnRpbmdlbmN5IG1hdHJpY2VzIHByZXBhcmVkIGZvciB0aGUgaGVhdC1tYXAgcGxvdHMgd2UgY2FuIGRvIGZ1cnRoZXIgYW5hbHlzaXMsIGxpa2UsIGZpbmRpbmcgY29ycmVsYXRpb25zIG9yIG5lYXJlc3QgbmVpZ2hib3JzLiAoU2VlIGJlbG93LikKCkNyb3NzLXRhYnVsYXRlIGRhdGVzIHdpdGggcmVnaW9uczoKCmBgYHtyfQphTWF0RGF0ZVJlZ2lvbiA8LSBwdXJycjo6bWFwKCBhUXVlcmllcywgZnVuY3Rpb24oZGZYKSB7IHh0YWJzKCBmb3JtdWxhID0gVmFsdWUgfiBEYXRlICsgcmVnaW9uLCBkYXRhID0gZGZYLCBzcGFyc2UgPSBUUlVFICkgfSApCmFNYXREYXRlUmVnaW9uIDwtIGFNYXREYXRlUmVnaW9uWyBwdXJycjo6bWFwX2xnbChhTWF0RGF0ZVJlZ2lvbiwgZnVuY3Rpb24oeCkgbnJvdyh4KSA+IDAgKSBdCmBgYAoKCgpgYGB7cn0KZGZQbG90UXVlcnkgPC0gcHVycnI6Om1hcF9kZiggYU1hdERhdGVSZWdpb24sIE1hdHJpeDo6c3VtbWFyeSwgLmlkID0gIlR5cGUiICkKaGVhZChkZlBsb3RRdWVyeSkKYGBgCgpgYGB7ciwgZmlnLndpZHRoID0gOCwgZmlnLmhpZ2h0ID0gOCwgd2FybmluZz1GQUxTRX0KZ2dwbG90Mjo6Z2dwbG90KGRmUGxvdFF1ZXJ5KSArCiAgZ2dwbG90Mjo6Z2VvbV90aWxlKCBnZ3Bsb3QyOjphZXMoIHggPSBqLCB5ID0gaSwgZmlsbCA9IGxvZzEwKHgpKSwgY29sb3IgPSAid2hpdGUiKSArCiAgZ2dwbG90Mjo6c2NhbGVfZmlsbF9ncmFkaWVudChsb3cgPSAid2hpdGUiLCBoaWdoID0gImJsdWUiKSArCiAgZ2dwbG90Mjo6eGxhYigiUmVnaW9uIikgKyBnZ3Bsb3QyOjp5bGFiKCJEYXRlIikgKyAKICBnZ3Bsb3QyOjpmYWNldF93cmFwKCB+VHlwZSwgc2NhbGVzID0gImZyZWUiLCBuY29sID0gMikKYGBgCgpIZXJlIHdlIHRha2UgYSAiY2xvc2VyIGxvb2siIHRvIG9uZSBvZiB0aGUgcGxvdHMgdXNpbmcgYSBkZWRpY2F0ZWQgYGQzaGVhdG1hcGAgcGxvdDoKCmBgYHtyfQpkM2hlYXRtYXA6OmQzaGVhdG1hcCggeCA9IGFNYXREYXRlUmVnaW9uW1siY291bnRyeS9yZWdpb24uZHJpdmluZyJdXSwgUm93diA9IEZBTFNFICkKYGBgCgojIE5lYXJlc3QgbmVpZ2hib3JzIGdyYXBocwoKIyMgR3JhcGhzIG92ZXJ2aWV3CgpIZXJlIHdlIGNyZWF0ZSBuZWFyZXN0IG5laWdoYm9yIGdyYXBocyBvZiB0aGUgY29udGluZ2VuY3kgbWF0cmljZXMgY29tcHV0ZWQgYWJvdmUgYW5kIHBsb3QgY2x1c3RlciB0aGUgbm9kZXM6CgpgYGB7cn0KdGggPC0gMC45NAphTk5HcmFwaHMgPC0gCiAgcHVycnI6Om1hcCggYU1hdERhdGVSZWdpb24sIGZ1bmN0aW9uKG0pIHsgCiAgICBtMiA8LSBjb3IoYXMubWF0cml4KG0pKQogICAgZm9yKCBpIGluIDE6bnJvdyhtMikgKSB7CiAgICAgIG0yW2ksaV0gPC0gMAogICAgfQogICAgbTIgPC0gYXMoIG0yLCAiZGdDTWF0cml4IikgCiAgICBtMkB4WyBtMkB4IDw9IHRoIF0gPC0gMAogICAgI20yQHhbIG0yQHggPiB0aCBdIDwtIDEKICAgIGlncmFwaDo6Z3JhcGhfZnJvbV9hZGphY2VuY3lfbWF0cml4KE1hdHJpeDo6ZHJvcDAobTIpLCB3ZWlnaHRlZCA9IFRSVUUsIG1vZGUgPSAidW5kaXJlY3RlZCIpCiAgfSkKYGBgCgpgYGB7ciwgZXZhbD1GQUxTRSwgd2FybmluZz1GQUxTRX0KaW5kIDwtIDMKY2ViIDwtIGNsdXN0ZXJfZWRnZV9iZXR3ZWVubmVzcyhhTk5HcmFwaHNbW2luZF1dKSAgCmRlbmRQbG90KGNlYiwgbW9kZT0iaGNsdXN0IiwgbWFpbiA9IG5hbWVzKGFOTkdyYXBocylbW2luZF1dKQpgYGAKCmBgYHtyLCBldmFsPUZBTFNFfQpwbG90KGNlYiwgYU5OR3JhcGhzW1tpbmRdXSwgdmVydGV4LnNpemU9MSwgdmVydGV4LmxhYmVsPU5BLCBtYWluID0gbmFtZXMoYU5OR3JhcGhzKVtbaW5kXV0pCmBgYAoKIyBUaW1lIHNlcmllcyBhbmFseXNpcwoKIyMgVGltZSBzZXJpZXMKCkluIHRoaXMgc2VjdGlvbiBmb3IgZWFjaCBkYXRlIHdlIHN1bSBhbGwgY2FzZXMgb3ZlciB0aGUgcmVnaW9uLXRyYW5zcG9ydGF0aW9uIHBhaXJzLCBtYWtlIGEgdGltZSBzZXJpZXMsIGFuZCBwbG90IHRoZW0uIAoKKipSZW1hcms6KiogSW4gdGhlIHBsb3RzIHRoZSBTdW5kYXlzIGFyZSBpbmRpY2F0ZWQgd2l0aCBvcmFuZ2UgZGFzaGVkIGxpbmVzLgoKSGVyZSB3ZSBtYWtlIHRoZSB0aW1lIHNlcmllczoKCmBgYHtyfQphRGF0ZVN0cmluZ1RvRGF0ZU9iamVjdCA8LSB1bmlxdWUoIGRmQXBwbGVNb2JpbGl0eUxvbmdGb3JtWywgYygiRGF0ZSIsICJEYXRlT2JqZWN0IildICkKYURhdGVTdHJpbmdUb0RhdGVPYmplY3QgPC0gc2V0TmFtZXMoIGFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0JERhdGVPYmplY3QsIGFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0JERhdGUgKQphRGF0ZVN0cmluZ1RvRGF0ZU9iamVjdCA8LSBhcy5QT1NJWGN0KGFEYXRlU3RyaW5nVG9EYXRlT2JqZWN0KQphVFNEaXJSZXFCeUNvdW50cnkgPC0gIHB1cnJyOjptYXAoIGFNYXREYXRlUmVnaW9uLCBmdW5jdGlvbihtKSByb3dTdW1zKG0pICkKYGBgCgpgYGB7cn0KbWF0VFMgPC0gZG8uY2FsbCggY2JpbmQsIGFUU0RpclJlcUJ5Q291bnRyeSkKYGBgCgpgYGB7cn0Kem9vT2JqIDwtIHpvbzo6em9vKCB4ID0gbWF0VFMsIGFzLlBPU0lYY3Qocm93bmFtZXMobWF0VFMpKSApCmBgYAoKSGVyZSB3ZSBwbG90IHRoZW06CgoKYGBge3IsIGZpZy5oZWlnaHQ9NiwgZmlnLndpZHRoPTZ9CmF1dG9wbG90KHpvb09iaikgKwogIGFlcyhjb2xvdXIgPSBOVUxMLCBsaW5ldHlwZSA9IE5VTEwpICsKCWZhY2V0X2dyaWQoU2VyaWVzIH4gLiwgc2NhbGVzID0gImZyZWVfeSIpICsKICBnZW9tX3ZsaW5lKCB4aW50ZXJjZXB0ID0gYURhdGVTdHJpbmdUb0RhdGVPYmplY3Rbd2Vla2RheXMoYURhdGVTdHJpbmdUb0RhdGVPYmplY3QpID09ICJTdW5kYXkiXSwgY29sb3IgPSAib3JhbmdlIiwgbGluZXR5cGUgPSAiZGFzaGVkIiwgc2l6ZSA9IDAuMyApCmBgYAoKCioqT2JzZXJ2YXRpb246KiogSW4gdGhlIHRpbWUgc2VyaWVzIHBsb3RzIHRoZSBTdW5kYXlzIGFyZSBpbmRpY2F0ZWQgd2l0aCBvcmFuZ2UgZGFzaGVkIGxpbmVzLiAKV2UgY2FuIHNlZSB0aGF0IGZyb20gTW9uZGF5IHRvIFRodXJzZGF5IHBlb3BsZSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoZWlyIHRyaXBzIHRoYW4gc2F5IG9uIEZyaWRheXMgYW5kIFNhdHVyZGF5cy4gCldlIGNhbiBhbHNvIHNlZSB0aGF0IG9uIFN1bmRheXMgcGVvcGxlIChvbiBhdmVyYWdlKSBhcmUgbW9yZSBmYW1pbGlhciB3aXRoIHRoZWlyIHRyaXBzIG9yIHNpbXBseSB0cmF2ZWwgbGVzcy4KCiMjIOKAnEZvcmVjYXN04oCdCgpIZSB3ZSBkbyDigJxmb3JlY2FzdOKAnSBmb3IgY29kZS13b3JrZmxvdyBkZW1vbnN0cmF0aW9uIHB1cnBvc2VzIC0tIHRoZSBmb3JlY2FzdHMgc2hvdWxkIG5vdCBiZSB0YWtlbiBzZXJpb3VzbHkuCgpGaXQgYSB0aW1lIHNlcmllcyBtb2RlbCB0byB0aGUgdGltZSBzZXJpZXM6CgpgYGB7cn0KYVRTTW9kZWxzIDwtIHB1cnJyOjptYXAoIG5hbWVzKHpvb09iaiksIGZ1bmN0aW9uKHgpIHsgZm9yZWNhc3Q6OmF1dG8uYXJpbWEoIHpvbyggeCA9IHpvb09ialsseF0sIG9yZGVyLmJ5ID0gaW5kZXgoem9vT2JqKSApICkgfSApCmBgYAoKYGBge3J9CmFUU01vZGVscyA8LSBwdXJycjo6bWFwKCBuYW1lcyh6b29PYmopLCBmdW5jdGlvbih4KSBmb3JlY2FzdDo6Zm9yZWNhc3QoIGFzLm1hdHJpeCh6b29PYmopWyx4XSApICkKbmFtZXMoYVRTTW9kZWxzKSA8LSBuYW1lcyh6b29PYmopCmBgYAoKUGxvdCBkYXRhIGFuZCBmb3JlY2FzdDoKCmBgYHtyfQpsc1Bsb3RzIDwtIHB1cnJyOjptYXAoIG5hbWVzKGFUU01vZGVscyksIGZ1bmN0aW9uKHgpIGF1dG9wbG90KGFUU01vZGVsc1tbeF1dKSArIHlsYWIoIlZvbHVtZSIpICsgZ2d0aXRsZSh4KSApCm5hbWVzKGxzUGxvdHMpIDwtIG5hbWVzKGFUU01vZGVscykKYGBgCgoKYGBge3J9CmRvLmNhbGwoIGdyaWRFeHRyYTo6Z3JpZC5hcnJhbmdlLCBsc1Bsb3RzICkKYGBgCgojIFJlZmVyZW5jZXMKCltBUFBMMV0gQXBwbGUgSW5jLiwgW01vYmlsaXR5IFRyZW5kcyBSZXBvcnRzXShodHRwczovL3d3dy5hcHBsZS5jb20vY292aWQxOS9tb2JpbGl0eSksICgyMDIwKSwgW2FwcGxlLmNvbV0oaHR0cHM6Ly93d3cuYXBwbGUuY29tKS4KCltBQTFdIEFudG9uIEFudG9ub3YsIApbIkFwcGxlIG1vYmlsaXR5IHRyZW5kcyBkYXRhIHZpc3VhbGl6YXRpb24iXShodHRwczovL2dpdGh1Yi5jb20vYW50b25vbmN1YmUvU3lzdGVtTW9kZWxpbmcvYmxvYi9tYXN0ZXIvUHJvamVjdHMvQ29yb25hdmlydXMtcHJvcGFnYXRpb24tZHluYW1pY3MvRG9jdW1lbnRzL0FwcGxlLW1vYmlsaXR5LXRyZW5kcy1kYXRhLXZpc3VhbGl6YXRpb24ubWQpLCAKKDIwMjApLCAKW1N5c3RlbU1vZGVsaW5nIGF0IEdpdEh1Yl0oaHR0cHM6Ly9naXRodWIuY29tL2FudG9ub25jdWJlL1N5c3RlbU1vZGVsaW5nKS4KCltBQTJdIEFudG9uIEFudG9ub3YsIApbIk5ZIFRpbWVzIENPVklELTE5IGRhdGEgdmlzdWFsaXphdGlvbiJdKGh0dHBzOi8vZ2l0aHViLmNvbS9hbnRvbm9uY3ViZS9TeXN0ZW1Nb2RlbGluZy9ibG9iL21hc3Rlci9Qcm9qZWN0cy9Db3JvbmF2aXJ1cy1wcm9wYWdhdGlvbi1keW5hbWljcy9Eb2N1bWVudHMvTllUaW1lcy1DT1ZJRC0xOS1kYXRhLXZpc3VhbGl6YXRpb24ubWQpLCAKKDIwMjApLCAKW1N5c3RlbU1vZGVsaW5nIGF0IEdpdEh1Yl0oaHR0cHM6Ly9naXRodWIuY29tL2FudG9ub25jdWJlL1N5c3RlbU1vZGVsaW5nKS4KCg==